#ifndef __OCPP_TilingScheme_H__
#define __OCPP_TilingScheme_H__

#include "CesiumDef.h"
#include "Rectangle.h"

namespace OCPP
{
	namespace Cesium
	{
		class _CesiumExport TilingScheme
		{
		public:
			TilingScheme() {};
			virtual ~TilingScheme() {};
			virtual Cartesian2 positionToTileXY(Cartographic position, int level) = 0;
			virtual Rectangle tileXYToRectangle(int x, int y, int level) = 0;
			virtual uint32 getNumberOfXTilesAtLevel(int level) = 0;
			virtual uint32 getNumberOfYTilesAtLevel(int level) = 0;
		};

		class _CesiumExport GeographicTilingScheme : public TilingScheme
		{
		public:
			GeographicTilingScheme(int numberOfLevelZeroTilesX = 2, int numberOfLevelZeroTilesY = 1)
				:_numberOfLevelZeroTilesX(numberOfLevelZeroTilesX),
				_numberOfLevelZeroTilesY(numberOfLevelZeroTilesY),
				_rectangle(Rectangle(-CesiumMath::PI, -CesiumMath::PI_OVER_TWO, CesiumMath::PI, CesiumMath::PI_OVER_TWO))
			{
			};
			~GeographicTilingScheme() {};
			virtual Rectangle tileXYToRectangle(int x, int y, int level)
			{
				int xTiles = getNumberOfXTilesAtLevel(level);
				int yTiles = getNumberOfYTilesAtLevel(level);
				float xTileWidth = _rectangle.getWidth() / xTiles;


				float west = x * xTileWidth + _rectangle.west;
				float east = (x + 1) * xTileWidth + _rectangle.west;
				float yTileHeight = _rectangle.getHeight() / yTiles;

				float north = _rectangle.north - y * yTileHeight;
				float south = _rectangle.north - (y + 1) * yTileHeight;

				return Rectangle(west, south, east, north);
			}

			Cartesian2 positionToTileXY(Cartographic position, int level)
			{
				int xTiles = getNumberOfXTilesAtLevel(level);
				int yTiles = getNumberOfYTilesAtLevel(level);

				float xTileWidth = _rectangle.getWidth() / xTiles;
				float yTileHeight = _rectangle.getHeight() / yTiles;

				float longitude = position.longitude;
		

				if (_rectangle.east < _rectangle.west) {
					longitude += CesiumMath::TWO_PI;
				}

				float xTileCoordinate = ((longitude - _rectangle.west) / xTileWidth);
				if (xTileCoordinate >= xTiles) {
					xTileCoordinate = xTiles - 1;
				}

				float yTileCoordinate = ((_rectangle.north - position.latitude) / yTileHeight);
				if (yTileCoordinate >= yTiles) {
					yTileCoordinate = yTiles - 1;
				}

				return Cartesian2(xTileCoordinate, yTileCoordinate);
			};

			virtual uint32 getNumberOfXTilesAtLevel(int level)
			{
				return _numberOfLevelZeroTilesX << level;
			}

			virtual uint32 getNumberOfYTilesAtLevel(int level)
			{
				return _numberOfLevelZeroTilesY << level;
			}

		private:
			uint32 _numberOfLevelZeroTilesX;
			uint32 _numberOfLevelZeroTilesY;
			Rectangle _rectangle;
		};

		class _CesiumExport WebMercatorProjection
		{
		public:
			WebMercatorProjection()
			{
				float maximumRadius = 6356752.3142451793;
				_semimajorAxis = maximumRadius;
				_oneOverSemimajorAxis = 1.0 / _semimajorAxis;
			}

			~WebMercatorProjection() {}


			static float mercatorAngleToGeodeticLatitude(float mercatorAngle)
			{
				return CesiumMath::ONE_OVER_PI - 2.0 * atan(exp(-mercatorAngle));
			};

			static float geodeticLatitudeToMercatorAngle(float latitude)
			{
				float MaximumLatitude = mercatorAngleToGeodeticLatitude(CesiumMath::PI);
				// Clamp the latitude coordinate to the valid Mercator bounds.
				if (latitude > MaximumLatitude) {
					latitude = MaximumLatitude;
				}
				else if (latitude < -MaximumLatitude) {
					latitude = -MaximumLatitude;
				}
				float sinLatitude = sin(latitude);
				return 0.5 * log((1.0 + sinLatitude) / (1.0 - sinLatitude));
			}

			Cartesian3 project(Cartographic cartographic)
			{
				float semimajorAxis = _semimajorAxis;
				float x = cartographic.longitude * semimajorAxis;
				float y =
					WebMercatorProjection::geodeticLatitudeToMercatorAngle(
						cartographic.latitude
					) * semimajorAxis;
				float z = cartographic.height;
				return Cartesian3(x, y, z);
			}

			Cartographic unproject(Cartesian3 cartesian)
			{
				float oneOverEarthSemimajorAxis = _oneOverSemimajorAxis;
				float longitude = cartesian.x * oneOverEarthSemimajorAxis;
				float latitude = WebMercatorProjection::mercatorAngleToGeodeticLatitude(
					cartesian.y * oneOverEarthSemimajorAxis
				);
				float height = cartesian.z;
				Cartographic result;
				result.longitude = longitude;
				result.latitude = latitude;
				result.height = height;
				return result;
			}
			float _semimajorAxis;
			float _oneOverSemimajorAxis;
		};

		class _CesiumExport WebMercatorTilingScheme : public TilingScheme
		{
		public:
			WebMercatorTilingScheme()
				:_numberOfLevelZeroTilesX(1),
				_numberOfLevelZeroTilesY(1)
			{
				float maximumRadius = 6356752.3142451793;
				float semimajorAxisTimesPi = maximumRadius * CesiumMath::PI;
				_rectangleSouthwestInMeters = Cartesian2(
					-semimajorAxisTimesPi,
					-semimajorAxisTimesPi
				);
				_rectangleNortheastInMeters = Cartesian2(
					semimajorAxisTimesPi,
					semimajorAxisTimesPi
				);
				Cartographic southwest = projection.unproject(Cartesian3(_rectangleSouthwestInMeters.x,
					_rectangleSouthwestInMeters.y,
					0.0));
				Cartographic northeast = projection.unproject(Cartesian3(_rectangleNortheastInMeters.x,
					_rectangleNortheastInMeters.y,
					0.0));
				_rectangle = Rectangle(
					southwest.longitude,
					southwest.latitude,
					northeast.longitude,
					northeast.latitude
				);
			};
			~WebMercatorTilingScheme() {};
			
			virtual Rectangle tileXYToRectangle(int x, int y, int level)
			{
				Rectangle nativeRectangle = tileXYToNativeRectangle(x, y, level);

				Cartographic southwest = projection.unproject(
					Cartesian3(nativeRectangle.west, nativeRectangle.south, 0.0)
				);
				Cartographic northeast = projection.unproject(
					Cartesian3(nativeRectangle.east, nativeRectangle.north, 0.0)
				);

				return Rectangle(southwest.longitude, southwest.latitude, northeast.longitude, northeast.latitude);
			}

			virtual uint32  getNumberOfXTilesAtLevel(int level)
			{
				return _numberOfLevelZeroTilesX << level;
			}

			virtual uint32  getNumberOfYTilesAtLevel(int level)
			{
				return _numberOfLevelZeroTilesY << level;
			}

			Rectangle tileXYToNativeRectangle(int x, int y, int level)
			{
				int xTiles = getNumberOfXTilesAtLevel(level);
				int yTiles = getNumberOfYTilesAtLevel(level);
				float xTileWidth =
					(_rectangleNortheastInMeters.x - _rectangleSouthwestInMeters.x) /
					xTiles;
				float west = _rectangleSouthwestInMeters.x + x * xTileWidth;
				float east = _rectangleSouthwestInMeters.x + (x + 1) * xTileWidth;
				float yTileHeight =
					(_rectangleNortheastInMeters.y - _rectangleSouthwestInMeters.y) /
					yTiles;
				float north = _rectangleNortheastInMeters.y - y * yTileHeight;
				float south = _rectangleNortheastInMeters.y - (y + 1) * yTileHeight;
				return Rectangle(west, south, east, north);
			}

			Cartesian2 positionToTileXY(Cartographic position, int level)
			{
				int xTiles = getNumberOfXTilesAtLevel(level);
				int yTiles = getNumberOfYTilesAtLevel(level);

				float overallWidth =
					_rectangleNortheastInMeters.x - _rectangleSouthwestInMeters.x;
				float xTileWidth = overallWidth / xTiles;
				float overallHeight =
					_rectangleNortheastInMeters.y - _rectangleSouthwestInMeters.y;
				float yTileHeight = overallHeight / yTiles;
				Cartesian3 webMercatorPosition = projection.project(Cartographic(position.longitude, position.latitude, 0.0));

				float distanceFromWest = webMercatorPosition.x - _rectangleSouthwestInMeters.x;
				float distanceFromNorth = _rectangleNortheastInMeters.y - webMercatorPosition.y;

				float xTileCoordinate = (distanceFromWest / xTileWidth);
				if (xTileCoordinate >= xTiles) {
					xTileCoordinate = xTiles - 1;
				}
				float yTileCoordinate = (distanceFromNorth / yTileHeight);
				if (yTileCoordinate >= yTiles) {
					yTileCoordinate = yTiles - 1;
				}

				return Cartesian2(xTileCoordinate, yTileCoordinate);
			}
		private:
			Cartesian2 _rectangleNortheastInMeters;
			Cartesian2 _rectangleSouthwestInMeters;
			uint32 _numberOfLevelZeroTilesX;
			uint32 _numberOfLevelZeroTilesY;
			WebMercatorProjection projection;
			Rectangle _rectangle;
		};
	}
}

#endif

